#include <stdio.h>

#include "portaudio.h"
#include "./cbuf.c"

#define SAMPLE_RATE 44100
#define PA_SAMPLE_TYPE paInt32
#define FRAMES_PER_BUFFER 64

#define CBUFSIZE 1024*1024 // circular buffer used by the callbacks to get and put samples

typedef int SAMPLE;

void openInputStream(void);
void closeInputStream(void);
void openOutputStream(void);
void closeOutputStream(void);

CircularBuffer inbuf;
CircularBuffer outbuf;
SAMPLE *samples; // small buffer used to pass samples between C and Go
PaStream *inStream, *outStream;

void
initPA(void)
{
	PaError err;

	// initialize circular buffers
	cbInit(&inbuf, CBUFSIZE);
	cbInit(&outbuf, CBUFSIZE);

	// initialize sample buffer
	samples = malloc(GOBUFSIZE * sizeof(SAMPLE));

	err = Pa_Initialize();
	if(err != paNoError) {
		fprintf(stderr, "error: %s\n", Pa_GetErrorText(err));
		return;
	}

	openInputStream();
	openOutputStream();
}

void
shutdownPA(void)
{
	cbFree(&inbuf);
	cbFree(&outbuf);
	free(samples);

	closeInputStream();
	closeOutputStream();

	Pa_Terminate();
}

static int
inputCallback(const void *inputBuffer, void *outputBuffer, unsigned long framesPerBuffer,
	const PaStreamCallbackTimeInfo* timeInfo, PaStreamCallbackFlags statusFlags, void *userData)
{
	SAMPLE *out = (SAMPLE*)outputBuffer;
	const SAMPLE *in = (const SAMPLE*)inputBuffer;
	unsigned int i;

	// unused
	(void) timeInfo, statusFlags, userData;

	// TODO: is it possible this is NULL?
	if(inputBuffer == NULL) {
		for(i=0; i<framesPerBuffer; i++)
			cbWrite(&inbuf, 0);
	} else {
		for (i=0; i<framesPerBuffer; i++)
			cbWrite(&inbuf, *in++);
	}
	return paContinue;
}

void
openInputStream(void)
{
	PaStreamParameters inputParameters;
	PaError err;

	inputParameters.device = Pa_GetDefaultInputDevice();
	if (inputParameters.device == paNoDevice) {
		fprintf(stderr,"Error: No default input device.\n");
		goto error;
	}
	inputParameters.channelCount = 1;
	inputParameters.sampleFormat = PA_SAMPLE_TYPE;
	inputParameters.suggestedLatency = Pa_GetDeviceInfo(inputParameters.device)->defaultLowInputLatency;
	inputParameters.hostApiSpecificStreamInfo = NULL;

	err = Pa_OpenStream(&inStream, &inputParameters, NULL, SAMPLE_RATE, FRAMES_PER_BUFFER, paClipOff, inputCallback, NULL);
	if(err != paNoError)
		goto error;

	err = Pa_StartStream(inStream);
	if(err != paNoError)
		goto error;

	return;

error:
	fprintf(stderr, "error: %s\n", Pa_GetErrorText(err));
	return;
}

void
closeInputStream(void)
{
	PaError err;

	err = Pa_CloseStream(inStream);
	if(err != paNoError)
		fprintf(stderr, "error: %s\n", Pa_GetErrorText(err));
}

SAMPLE *
getSamples()
{
	int i = 0;
	for (;;) {
		while (!cbIsEmpty(&inbuf)) {
			if (i < GOBUFSIZE) {
				samples[i] = cbRead(&inbuf);
			} else {
				goto full;
			}
			i++;
		}
		Pa_Sleep(50); // wait for some more samples
	}

full:
	return samples;
}

static int
outputCallback(const void *inputBuffer, void *outputBuffer, unsigned long framesPerBuffer,
	const PaStreamCallbackTimeInfo* timeInfo, PaStreamCallbackFlags statusFlags, void *userData)
{
	SAMPLE *out = (SAMPLE*)outputBuffer;

	// unused
	(void) inputBuffer, timeInfo, statusFlags, userData;

	int i;
	for (i = 0; i < framesPerBuffer; i++) {
		if (!cbIsEmpty(&outbuf))
			*out++ = cbRead(&outbuf);
	}

	return paContinue;
}

void
openOutputStream(void)
{
	PaStreamParameters outputParameters;
	PaError err;

	outputParameters.device = Pa_GetDefaultOutputDevice();
	if (outputParameters.device == paNoDevice) {
		fprintf(stderr,"Error: No default output device.\n");
		goto error;
	}
	outputParameters.channelCount = 1;
	outputParameters.sampleFormat = PA_SAMPLE_TYPE;
	outputParameters.suggestedLatency = Pa_GetDeviceInfo(outputParameters.device)->defaultLowInputLatency;
	outputParameters.hostApiSpecificStreamInfo = NULL;

	err = Pa_OpenStream(&outStream, NULL, &outputParameters, SAMPLE_RATE, FRAMES_PER_BUFFER, paClipOff, outputCallback, NULL);
	if(err != paNoError)
		goto error;

	err = Pa_StartStream(outStream);
	if(err != paNoError)
		goto error;

	return;

error:
	fprintf(stderr, "error: %s\n", Pa_GetErrorText(err));
	return;
}

void
closeOutputStream(void)
{
	PaError err;

	err = Pa_CloseStream(outStream);
	if (err != paNoError)
		fprintf(stderr, "error: %s\n", Pa_GetErrorText(err));
}

void
putSamples(SAMPLE *samples)
{
	int i;
	for (i = 0; i < GOBUFSIZE; i++)
		cbWrite(&outbuf, samples[i]);
}